﻿#pragma once
//远程指令程序：
//
//依旧是松耦合 所以我们另起一个类 采取回调的方式
//
//我们之前的自定义shell程序的思路不再适用！因为我们是多线程的前提，会导致每个线程执行命令的时候去调用exec接口，进行进程替换，会使整个进程崩溃！
//
//本质：exec() 系列函数会替换掉当前进程的整个地址空间（代码、数据、堆栈等），这会导致进程中所有其他线程“瞬间蒸发”，从而破坏整个程序的逻辑。
//
//而线程执行fork 和 单进程使用fork是一样的
//
//------------------------------
//我们使用新的方法来在多线程的前提是实现远程指令程序！
//
//popen：创建管道和子进程，子进程执行命令，然后返回值为file* 方便读取
//pclose：关闭
//
//下面来自deepseek的解释
//------------------------------------------------------------------------------------ -
//Q：popen不是和 fork() + pipe() + exec() + dup2() 操作类似吗？
//A：其设计初衷就是用于执行命令：popen() 是专门为“执行一个外部命令并读取其输出”这个场景设计的。它在内部安全地处理了 fork 和 exec 的流程。
//
//总结一下 popen() 的安全逻辑：
//它在你的多线程服务器进程中，创建了一个短暂的、独立的、单线程的“外部命令执行器”。这个执行器生来就是为了执行 ls、pwd 等命令，执行完就自我毁灭。它不会企图去替换或影响庞大的、多线程的服务器主进程。
//也就是线程只负责IO，执行命令的任务交给了 popen 管理的独立子进程。
//
//
//Q：我还是不懂 都是会进行程序替换的 凭什么popen就可以安全？
//
//你的方案（fork + exec）：在「公司总部」里搞拆迁
//想象你的服务器进程是一个公司总部大楼，里面有很多员工(线程)在协同工作
//
//你的方案：fork() + exec()
//fork() : 相当于把整个公司总部大楼复制了一份，创建了一个一模一样的分公司
//
//这个分公司里有原公司的所有部门、所有员工、所有资料的完整副本。
//然后你在分公司里调用 exec()：这相当于说"把这个分公司大楼炸掉，在原地重新建一个卖煎饼的小摊"。
//
//问题来了：虽然你炸的是分公司，但这个分公司是总部大楼的完整副本啊！万一在复制的瞬间，总部某个重要部门正在处理关键业务，这个状态也被复制过去了，然后你把它炸了，这肯定会出各种奇怪的问题。
//
//--------------------------------------
//popen的方案：在「空地上」新建
//
//popen() 的做法完全不同：
//popen() 内部也调用了 fork()，创建了一个子进程。
//
//关键区别：这个子进程立即调用 exec()，几乎没有任何业务逻辑。
//
//相当于：
//
//我先找一块空地（新建进程）
//
//直接在这块空地上建一个卖煎饼的小摊（exec）
//
//根本不存在"复制总部大楼再炸掉"的过程
//
//你的方案的问题在于：从 fork() 到 exec() 之间有一个时间窗口，在这个窗口期内：
//
//子进程是服务器进程的完整副本
//
//包含了所有线程的状态、锁状态、内存状态
//
//如果这时其他线程正在操作共享资源，子进程会复制到不一致的状态
//
//而 popen() 的方案中：
//
//fork() 后几乎立即 exec()，时间窗口极短
//
//子进程几乎不执行任何业务逻辑
//
//没有机会遇到多线程同步问题
//------------------------------------------------------------------------------------ -
//其次 我们仅仅只允许执行有限的安全的命令，杜绝远程高危命名的执行
//
//我们把能执行的命名都放在set中 只有这里面的命令才可以被执行
//
//弄一个类似词典的配置文件 加载到set中(构造函数中调用加载函数)
//
//最后记得main.cc中bind 让服务器类和命令类bind在一起
//
//这个代码缺少去除选项部分 会导致即使白名单中有touch 也无法执行touch
//所以我们在safecheck函数中 不能直接检查 而是先提取一下客户端发来的命令中的指令部分(提取前缀)，很简单 就是第一个空格前面的字符串 就是关键部分！
//
//细节在于 可能用户只输入了一个字符串  不如单独的ls  或 cat 这种 直接return即可！这种不需要提取
//
//---------------------------------------- -
//recv和send 是有问题的  标志位参数需要正确的使用
//问题在于有可能读取"ls -a -l"的时候  没有完整的读取完毕 因为是面向字节流
//当命令字符串较长的时候 就有可能出现这种情况
//
//而UDP的时候 我们没问题 是因为 其是面向数据报的 就好比收快递 要么就收不到 要么收到就是完整的！所以不会出现收到一部分的情况！
//
//而且我们之前对文件进行读写操作的时候，我们写的时候，可以很爽，单数读的时候 则很别扭  读多少 怎么读  读出来还要转化为什么等问题....
//---------------------------------------- -
//三次握手，四次挥手，才能建立连接和断开连接  初认识！
//---------------------------------------- -
//序列化和反序列化的初认识
//---------------------------------------- -
//重新理解read write recv send  以及tcp 为什么是全双工？
//
//
//首先write send 本质是拷贝函数！把数据拷贝到了TCP，也就是传输层的缓冲区中，而怎么发 什么时候发  出错了怎么办 完全由TCP协议决定 所以TCP叫做传输控制协议！控制就体现在这里
//
//
//就好比文件的write也是拷贝到内核的缓冲区，有内核控制刷新
//
//而TCP也属于内核 我们说过TCP是实现在内核的 所以道理类似！
//
//所以所谓的发送数据 其实就是从主机A的传输层的发送缓冲区通过协议栈和网络拷贝到了主机B的传输层的接收缓冲区中 然后由read或recv读取！
//
//所以网络通信在传输层 其实是双方的OS在进行通信 内核进行！这也是为什么说用户不用担心网络的细节 只用在应用层写代码！
//
//其实调用read或recv的本质就是从接收缓冲区拷贝到了用户层的缓冲区罢了
//而当接收缓冲区无数据 则read和recv就会阻塞！！！
//
//而发送缓冲区写满了 也会阻塞 所以send或write会阻塞！
//
//所以进程才会阻塞，其才会被设置为S状态，放在某个队列中，直到接收缓冲区有数据，才会被唤醒
//
//所以阻塞的本质就是，用户层在进行同步！也就是必须有数据才能读，有空间才能写！
//
//所以对于发送缓冲区 用户生产 OS消费 接受缓冲区 用户消费 OS生产
//
//所以TCP发送数据的本质就是两个缓冲区之间的拷贝
//而通信的本质就是更多缓冲区之间的拷贝！！
//
//而一个主机的传输层有两个缓冲区 所以主机A和主机B可以同时向对方发送信息，这就是为什么是全双工！！！
//
//而UDP也是全双工 但是道理有些许区别！
//
//这也是为什么我们可以在应用层通过socket的fd 既读又写！因为读写对应不同的缓冲区！
//------------------------------------------
//为什么recv会有bug 也就是读取不完全 recv的bug怎么处理
//
//会有bug的原因就是发送方的发送缓冲的内容大小远大于接收方的接收缓冲区所剩余的空间！导致对方接收缓冲区只接收到了一部分！
//
//其次也有可能是你的write的内容大于了你的传输层的发送缓冲区的剩余空间，所以都是有可能出bug的！
//
//而怎么解决，必然和传输层的缓冲区没有关系，因为缓冲区只看到了一串字节流，只有应用层才能解决这个问题！
//
//而我们关于读写不就只有recv 和 send吗，所以这就是最后一个参数的作用！
//
//------------------------------------------
//⽹络版计算器的实现：
//
//采取⽅案⼆ :
//• 定义结构体来表⽰我们需要交互的信息;
//• 发送数据时将这个结构体按照⼀个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
//• 这个过程叫做 "序列化" 和 "反序列化
//
//
//模版方法模式 去 封装套接字！
//
//①：封装一下套接字  也就是几个接口之间组合的封装
//其中的关键在于为什么ACcepter接口需要返回一个sock类的指针，这是因为我们可以在sock类中实现封装recv和send的接口 ，从而直接通过->调用
//
//因为我们之前的Loop函数中进行accept之后，会用其返回的fd去进行读写函数的调用，而现在我们将这个fd封装为类指针之后，直接->调用读写成员函数
//
//重点：我们的tcpsocket类是继承自socket类 所以我们返回的是基类的指针
//所以我们的fd封装的类指针就是基类的指针
//
//所以我们的Accepter函数 输出型参数得到了客户端的地址 返回值是基类的指针，此时就可以正确的使用该输出型参数给基类的成员函数进行传参调用了！
//
//这样封装下来 就能避免直接使用fd  和 一些接口 取决于封装成本
//所以REcv 和 Send 先不写
//
//同理Coneect接口也要返回基类指针 还是bool类型把？一会解释
//
//所以 一系列的封装之后，我们从此之后的服务器类和客户端 会简洁很多！
//
//唯一的疑问 在于为什么能用基类指针来接受？后面自己思考
//
//
//②：服务类的改动
//除开代码简洁之外，服务类的成员变量肯定要新增一个socket类的指针，其次socket类去调用成员函数的时候，参数也需要网络属性结构体，但是我们的服务类直接收到了一个port。所以我们的inetaddr类需要新增一种构造函数的方法，只需要port 也能 构造出一个inetaddr的对象，其实IP也传递了，不过手动写的"0"
//
//所以之前的服务类的成员变量port 变成了网络属性结构体类 inetaddr类型！
//
//这样我们就可以通过_listensocket变量去调用socket类中的成员函数了，该函数需要的参数就可以直接传递_localaddr，也就是inetaddr类型变量
//
//依旧是智能指针和基类的穿插理解没搞懂 后面思考！
//
//而现在的问题在于我们的server函数的参数是int的fd 和 网络属性结构体 而我们的线程函数中已经无法再给予server函数需要的参数 之前可以通过线程函数的参数 去->取到fd 等变量 传递给server函数 但是现在我的accept已经被封装成为了成员函数返回的是 socket类的指针
//
//所以我们想到的是从 socket类的指针取出成员变量fd  但是根本没有fd变量
//所以我们直接把server函数的参数中的fd改为 socket类的指针！
//
//而server函数的全部代码 全部封装进 socket类的成员函数中即可！松耦合！
//
//当然我们的服务器类中也是需要一个函数去回调在另外一个类中被封装的serveer 函数的 这个函数类型就是io_server_t 到时候再绑定即可
//
//必然的io_server_t 是成员变量！
//
//所以线程函数中的逻辑不用更改 但是问题在于server 的封装怎么做，你再怎么也需要给server内部的读写函数提供int类型的fd呀？！ 后面再讲
//
//
//③：引入协议
//在封装之前 我们需要将server的内部代码修改正确，因为我们的读写是有bug的，现在仅仅只能把读改写正确！
//
//前提是我们需要规定出一个类型 该类型是读取的基本单位 才能使得计算的题目 完全被读取！基本单位就是一个结构体，就是协议，就是约定！
//
//而该结构体不能直接通过网络传输 需要先序列化成一个字符串 再反序列化得到一个结构体！
//
//而该结构体 也就是该class类  在服务端和客户端都包含了 就是约定！
//
//Q：结构体和序列化之后的字符串不都是字节吗，对于tcp来说都是字节流，那有什么必要，不如直接传送结构体？
//A：可以但不建议这么做！
//技术理由：未来的客户端和服务端不一定在同一个OS系统中，内存对齐不一样，导致大小不一样，以及大小端差异的问题，跨平台性太差！
//业务理由：如果将来的结构体的格式或者变量数量发生变化，需要考虑的跨平台问题更加严重，在应用层去维护是费时费力的！
//
//所以我们只需把序列化写的更加通用一点即可！
//
//所以我们的协议就是：报文 = 长度 + 分隔符 + 有效载荷 + 分隔符
//如果连第一个分割符都没读到，证明不完整
//.....所有不完整都可以规避掉！！
//
//------------------------------------------------------------------------------------ - Q1：在Socket.hpp中，TcpSocket类中的Accepter函数中的这一句代码：socket_sptr sock = std::make_shared<TcpSocket>(sockfd);  疑问在于为什么能这么写，为什么要这么写？
//
//A1：这样可以通过基类指针调用虚函数时，实际执行的是对象实际类型（派生类）的函数实现，而不是基类的实现。这就是多态的核心意义。也就是方便调用其他在派生类中实现的函数了
//
//Q： 既然调用的是派生类的成员函数 为什么Accepter不直接返回派生类的指针？
//A：
//class Socket {
//public:
//    // 如果这样声明，就绑死到TcpSocket了
//    virtual std::shared_ptr<TcpSocket> Accepter(InetAddr* addr) = 0;
//};
//
//你永远无法使用其他的类再次继承基类
//
//
//
//Q2：我不懂服务类的代码中 std::unique_ptr<Socket> _listensock;  和   _listensock(std::make_unique<TcpSocket>()), 为什么这里也需要把一个派生类指针赋给一个基类指针类型？？
